//build: 20240301
//The QR code system rules for micro-light include 101, 103, etc.
import common from './dxCommon.js'
import base64 from './dxBase64.js'
import logger from './dxLogger.js'
let sqliteObj

// Compare whether the first N characters of two strings are equal.
function comparePrefix(str1, str2, N) {
    let substring1 = str1.substring(0, N);
    let substring2 = str2.substring(0, N);
    return substring1 === substring2;
}



// 101 code value decoding
function decode101(str) {
    if (str.length < 5) {
        logger.info("Invalid QR code, length too short")
        throw new Error("code invalid,length too short")
    }
    let decodeBuf = base64.decode(str.slice(0, -4))
    decodeBuf=decodeBuf.substring(4)

    return decodeBuf
}
function hexStringToArrayBuffer (hexString) {
    var byteString = hexString.match(/.{1,2}/g);
    var byteArray = byteString.map(function (byte) {
        return parseInt(byte, 16);
    });
    var buffer = new Uint8Array(byteArray).buffer;
    return buffer;
}

/**
 * 103 code value decoding
 * 1. Base64 decoding
 * 2. Parse organization number
 * 3. RSA decryption
 * 4. Parse identity type
 * 5. Resolve permission identifiers
 * 6. Parse the generated code time
 * 7. Parser Expiration Time
 * 8. Verify if the access code has expired.
 * @param {*} str 
 * @returns 
 */
function decode103(str) {
    // The FIXME pubKey will need to be retrieved from the configuration later.
    let TLV_T_SIZE = 2, TLV_L_SIZE = 2, offset = 0, code, decryptedData, generationCodeTime, expirationTime

    // 1. Base64 decoding
    let decodeBuf = base64.toHexString(str)
    decodeBuf= hexStringToArrayBuffer(decodeBuf)
    let view = new Uint8Array(decodeBuf)
    let organizationNumber;
    // 2. Parse organization number
    if (view[offset] == 0x01) {
        offset += TLV_T_SIZE
        let orgNumLen = view[offset]
        offset += TLV_L_SIZE
        let orgNumBuf = new Uint8Array(decodeBuf, offset, orgNumLen);
        organizationNumber = String.fromCharCode.apply(null, new Uint8Array(orgNumBuf));
        logger.info("Organization Number: " + organizationNumber)
        offset += orgNumLen
    } else {
        throw new Error("code invalid,organization number error")
    }

    // 3. RSA decryption
    if (view[offset] == 0x02) {
        // Organization ID data length
        offset += TLV_T_SIZE
        let cipherTextLen = view[offset]
        offset += TLV_L_SIZE
        // RSA decryption of ciphertext
        let encryptedData = decodeBuf.slice(offset, offset + cipherTextLen)


        // The RSA query key (which can be fixed or written in a text file) is used to decrypt the code again.
        // RSA decrypted data
        let arr = sqliteObj.securityFindAllByCodeAndTypeAndTimeAndkey(undefined, undefined, undefined, Math.floor(Date.parse(new Date()) / 1000), organizationNumber)
        if (arr && arr.length > 0) {
            for (let data of arr) {
                decryptedData = common.arrayBufferRsaDecrypt(encryptedData, data.value)
                if (decryptedData != null) {
                    break
                }
            }
        }
        if (!arr && arr.length <= 0 || decryptedData == null) {
            return str
        }
    }

    offset = 1;
    view = new Uint8Array(decryptedData)
    // 4. Parse identity type (type:103)
    if (view[offset] == 0x03) {
        // Identity type data length
        offset += TLV_T_SIZE
        let identityTypeLength = view[offset]
        // Identity type data
        offset += TLV_L_SIZE
        let identityTypeBuf = new Uint8Array(decryptedData, offset, identityTypeLength);
        let identityType = String.fromCharCode.apply(null, identityTypeBuf);
        offset += identityTypeLength
        logger.info("Identity type data: " + identityType)
    }


    // 5. Parse the permission identifier (code)
    if (view[offset] == 0x04) {
        // Permission identifier data length
        offset += TLV_T_SIZE
        let identityCodeLength = view[offset]
        // Permission identification data
        offset += TLV_L_SIZE
        let identityCodeBuf = new Uint8Array(decryptedData, offset, identityCodeLength);
        offset += identityCodeLength
        code = String.fromCharCode.apply(null, identityCodeBuf);
    }


    // 6. Parse the generated code time
    if (view[offset] == 0x05) {
        // Generation time data length
        offset += TLV_T_SIZE
        let createTimeLength = view[offset]
        // Generation time data
        offset += TLV_L_SIZE
        let createTimeBuf = new Uint8Array(decryptedData, offset, createTimeLength);
        offset += createTimeLength
        generationCodeTime = parseInt(common.arrayBufferToHexString(createTimeBuf.reverse()), 16)
    }


    // 7. Parser Expiration Time
    if (view[offset] == 0x06) {
        // Code expiration time data length
        offset += TLV_T_SIZE
        let expireTimeLength = view[offset]
        // Code expiration time data
        offset += TLV_L_SIZE
        let expireTimeBuf = new Uint8Array(decryptedData, offset, expireTimeLength);
        offset += expireTimeLength
        expirationTime = parseInt(String.fromCharCode.apply(null, expireTimeBuf))
    }

    // 8. Verify if the access code has expired.
    let timestamp = Date.now();
    expirationTime = generationCodeTime + expirationTime
    if (expirationTime * 1000 > timestamp) {
        return code
    } else {
        return null
    }

}

const code = {
    formatCode: function (msg, sqlObj) {
        if (!msg) {
            throw new Error("msg should not be null or empty")
        }
        if (!sqlObj) {
            throw new Error("sqlObj should not be null or empty")
        }
        if (!sqliteObj) {
            sqliteObj = sqlObj
        }

        let data = {}
        // Determine the code value
        if (comparePrefix(msg, "&llgy", "&llgy".length) || comparePrefix(msg, "&v101", "&v101".length)) {
            // 101 code value
            data.type = 101
            data.code = decode101(msg.substring(5))
        }
        else if (comparePrefix(msg, "vg://v103", "vg://v103".length)) {
            // 103 code value
            data.type = 103
            data.code = decode103(msg.substring(9)) ? decode103(msg.substring(9)) : msg.substring(9)
        } else if (comparePrefix(msg, "___VBAR_CONFIG_V1.1.0___", "___VBAR_CONFIG_V1.1.0___".length) || comparePrefix(msg, "___VBAR_KZB_V1.1.0___", "___VBAR_KZB_V1.1.0___".length)) {
            data.type = 'config'
            data.code = msg
        } else {
            data.type = 100
            data.code = msg
        }
        if (data.code) {
            return data
        } else {
            console.log("decode fail")
            return
        }
    }
}

export default code;